<?php
namespace modules\auth;
use \modules\org\storage as org;
use \phiction\exceptions\login as login_required;
use \phiction\exceptions\denied as access_denied;
use \phiction\exceptions\access_control;
use \phiction\exceptions\bad_request;
use \phiction\array_utils as arr;

class api
{
    static function register($q, &$args)
    {
        $orgname  = arr::get_or_bad_request($args,  'orgname');
        $codename = arr::get_or_bad_request($args, 'codename');
        $name     = arr::get_or_bad_request($args,     'name');
        $username = arr::get_or_bad_request($args, 'username');
        $password = arr::get_or_bad_request($args, 'password');

        $org = org::id_from_codename($q, $codename)->run();
        if (!is_null($org)) throw new bad_request("组织代号已存在");

        if (org::add($q, $codename, $orgname)->run() !== 1)
            throw new bad_request("cannot add organization");

        $org = org::id_from_codename($q, $codename)->run();
        if (is_null($org)) throw new bad_request("unknown error: cannot find the newly created organization");
        $org_id = arr::get_or_bad_request($org, 'id');

        if (storage::add_admin($q,
                $username, $password, $name, '', $org_id)->run() !== 1) {
            throw new bad_request("cannot add initial admin in organization");
        }

        $info = storage::info_from_username($q, $username, $codename)->run();
        if (is_null($info)) throw new bad_request("unknown error: cannot find the newly created admin");

        $info['username'] = $username;
        $token = self::issue_token_from_array($info);
        $args['token'] = $token;
    }

    static function logout($q, &$args)
    {
        $args['token'] = '';
    }

    # authentication: login
    static function login($q, &$args)
    {
        $codename = arr::get_or_bad_request($args, 'codename');
        $username = arr::get_or_bad_request($args, 'username');
        $password = arr::get_or_bad_request($args, 'password');
        $errmsg = "组织代号、用户名或密码错误。";

        $info = storage::info_from_username($q, $username, $codename)->run();
        if (is_null($info)) throw new bad_request($errmsg);

        $active = $info['active'];
        $credential = $info['credential'];

        if (!security::verify_password($password, $credential))
            throw new bad_request($errmsg);

        if (!$active) throw new bad_request("已离职用户不能登录");

        $info['username'] = $username;
        $token = self::issue_token_from_array($info);
        $args['token'] = $token;
    }

    static function revoke_my_token($q, &$args)
    {
        $user = self::validate($q, $args);
        $id   = $user['id'];
        $org  = $user['org'];

        if (storage::revoke_token($q, $org, $id)->run() !== 1)
            throw new bad_request("剥夺登录令牌失败");

        self::logout($q, $args);
    }

    static function revoke_token($q, &$args)
    {
        $id = (int)arr::get_or_bad_request($args, 'id');

        $user = self::validate_with_role($q, $args, 'admin');
        $org = $user['org'];

        if ($user['id'] === $id)
            throw new bad_request("不能使用 revoke-token 剥夺自己的登录令牌，请调用 revoke-my-token");

        if (storage::revoke_token($q, $org, $id)->run() !== 1)
            throw new bad_request("剥夺登录令牌失败");
    }


    # authentication: validate
    static function validate($q, &$args): array
    {
        $token = arr::get_or_throw($args, 'token', new login_required);

        try {
            $shards = security::break_token($token);
        }
        catch (\Throwable $ex) {
            error_log("BEGIN BAD TOKEN\e[1;31m");
            error_log($ex);
            error_log("\e[0mEND BAD TOKEN");
            throw new login_required;
            // invalid token should just ask the user to login
        }

        // make sure these fields are available
        $seq     = arr::get_or_throw($shards, 'seq',     new login_required);
        $refresh = arr::get_or_throw($shards, 'refresh', new login_required);
        arr::get_or_throw($shards, 'id',       new login_required);
        arr::get_or_throw($shards, 'username', new login_required);
        arr::get_or_throw($shards, 'role',     new login_required);
        arr::get_or_throw($shards, 'org',      new login_required);
        arr::get_or_throw($shards, 'name',     new login_required);
        arr::get_or_throw($shards, 'orgname',  new login_required);
        // as for why throw login_required instead of bad_request:
        //   if the token is valid, but there are some fields missing,
        //   the server must have been updated, so just let the user
        //   re-login to obtain a new version of the token
        //   by throwing login_required.

        if (time() > $refresh) {     // validate by database required
            $id = storage::id_from_seq($q, $seq)->run();
            if (is_null($id)) throw new login_required;
            // "seq" not found in database means that
            // the token has been revoked. in that case,
            // just let the user re-login by throwing login_required.

            $token = self::issue_token_from_array($shards);
            $args['token'] = $token;
        }

        return $shards;
    }

    // validate helper
    static function validate_with_role($q, &$args, string $expect_role): array
    {
        $shards = self::validate($q, $args);
        if ($shards['role'] !== $expect_role) throw new access_denied;
        return $shards;
    }

    // check whether the password can be used to login the current user
    static function validate_with_password($q, &$args, string $password): array
    {
        $shards = self::validate($q, $args);
        $seq = $shards['seq'];

        $info = storage::info_from_seq($q, $seq)->run();
        if (is_null($info)) throw new login_required;

        $active = $info['active'];
        $credential = $info['credential'];

        if (!$active) throw new bad_request("用户已离职");
        if (!security::verify_password($password, $credential))
            throw new bad_request("密码错误");

        return $shards;
    }

    // return null if not logged in
    // otherwise return shards
    static function login_status($q, &$args)
    {
        try {
            return self::validate($q, $args);
        }
        catch (access_control $ex) {
            return null;
        }
    }

    private static function issue_token(
        int $id,
        string $username,
        string $role,
        string $seq,
        int $org,
        string $name,
        string $orgname): string
    {
        return security::make_token([
            'id' => $id,
            'username' => $username,
            'role' => $role,
            'seq' => $seq,
            'org' => $org,
            'name' => $name,
            'orgname' => $orgname,
        ]);
    }

    private static function issue_token_from_array(array $info): string
    {
        return self::issue_token(
            arr::get_or_bad_request($info, 'id'),
            arr::get_or_bad_request($info, 'username'),
            arr::get_or_bad_request($info, 'role'),
            arr::get_or_bad_request($info, 'seq'),
            arr::get_or_bad_request($info, 'org'),
            arr::get_or_bad_request($info, 'name'),
            arr::get_or_bad_request($info, 'orgname'));
    }
}

